﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SynapseGaming.LightingSystem.Rendering;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System.Diagnostics;
using The_LarX.Rendering;

namespace The_LarX
{
    public class BlurUtilities : Component, IPostProcessor, IAnimateableFloat
    {
        public Camera3D Camera
        {
            get { return Renderer.Camera; }
        }

        public EditorRepresentation Representation;

        /// <summary>
        /// Blur Amount - Animation - For IAnimateable Interface
        /// </summary>
        public Animation_Float Animation { get; set; }

        public bool PlayBlur
        {
            get { return Animation.Play; }
            set { Animation.Play = value; }
        }

        public Animation_Float DistanceAnimation { get; set; }

        public bool PlayDistance
        {
            get { return DistanceAnimation.Play; }
            set { DistanceAnimation.Play = value; }
        }

        public Animation_Float RangeAnimation { get; set; }

        //////////////////////////////////
        //    BLUR                      //
        // 1. Render Screen             //
        // 2. Blur Horizontally         //
        // 3. Blur Vertically           //
        // 4. Render Depth              //
        // 5. Draw DOF                  //
        //////////////////////////////////

        public Render2Texture RenderTarget1; /// Blur
        public Render2Texture RenderTarget2; /// Targets
        public Render2Texture SunBurnRender; // SunBurn Render
        public Render2Texture DepthMapTarget; // Depth
        
        private Effect GaussianBlurEffect;
        private Effect DepthMapEffect;
        private Effect DepthOfFieldEffect;

        public float Distance
        {
            get { return DepthOfFieldEffect.Parameters["Distance"].GetValueSingle(); }
            set { DepthOfFieldEffect.Parameters["Distance"].SetValue(value); }
        }
        public float Range
        {
            get { return DepthOfFieldEffect.Parameters["Range"].GetValueSingle(); }
            set { DepthOfFieldEffect.Parameters["Range"].SetValue(value); }
        }
        
        public Renderer_SunBurn Renderer;

        public Viewport Viewport;

        public float BlurAmount { get; set; }

        public BlurUtilities(Renderer_SunBurn renderer, GameScreen gs)
            : base(gs)
        {
            this.Viewport = Engine.GraphicsDevice.Viewport;
            this.Renderer = renderer;

            this.Animation = new Animation_Float(SetBlurAmount, GetBlurAmount);
            this.DistanceAnimation = new Animation_Float(SetDistance, GetDistance);
            this.RangeAnimation = new Animation_Float(SetRange, GetRange);

            this.Representation = new EditorRepresentation("Engine\\Models\\model_post_processor", Renderer);
            this.Representation.Actor.ComponentParent = this;
            this.Representation.Actor.Parent = this;

            GaussianBlurEffect = Engine.Content.Load<Effect>("Engine\\Effects\\PostProcessors\\fx_GaussianBlur");
            DepthMapEffect = Engine.Content.Load<Effect>("Engine\\Effects\\DepthMap");
            DepthOfFieldEffect = Engine.Content.Load<Effect>("Engine\\Effects\\PostProcessors\\fx_DepthofField");
            renderer.SceneInterface.PostProcessManager.AddPostProcessor(this);

            BlurAmount = 1;

            SunBurnRender = new Render2Texture(Viewport.Width, Viewport.Height);
            RenderTarget1 = new Render2Texture(Viewport.Width, Viewport.Height);
            RenderTarget2 = new Render2Texture(Viewport.Width, Viewport.Height);
            DepthMapTarget = new Render2Texture(Viewport.Width, Viewport.Height);
        }

        public override void Update()
        {
            base.Update();

            if (Engine.PlayMode)
            {
                Animation.Update();
                DistanceAnimation.Update();
                RangeAnimation.Update();
            }

            Representation.Update();
        }
        public override void Draw()
        {
            base.Draw();

            Representation.Draw();
        }

        public void SetBlurAmount(float v)
        {
            this.BlurAmount = v;
        }
        public float GetBlurAmount()
        {
            return BlurAmount;
        }
        public void SetRange(float v)
        {
            this.Range = v;
        }
        public float GetRange()
        {
            return Range;
        }
        public void SetDistance(float v)
        {
            this.Distance = v;
        }
        public float GetDistance()
        {
            return Distance;
        }

        public BlurUtilities()
        {
            GaussianBlurEffect = Engine.Content.Load<Effect>("Engine\\Effects\\PostProcessors\\fx_GaussianBlur");
            DepthMapEffect = Engine.Content.Load<Effect>("Engine\\Effects\\DepthMap");
            DepthOfFieldEffect = Engine.Content.Load<Effect>("Engine\\Effects\\PostProcessors\\fx_DepthofField");

            BlurAmount = 1;

            this.Animation = new Animation_Float(SetBlurAmount, GetBlurAmount);
            this.DistanceAnimation = new Animation_Float(SetDistance, GetDistance);
            this.RangeAnimation = new Animation_Float(SetRange, GetRange);

            Viewport = Engine.GraphicsDevice.Viewport;
            SunBurnRender = new Render2Texture(Viewport.Width, Viewport.Height);
            RenderTarget1 = new Render2Texture(Viewport.Width, Viewport.Height);
            RenderTarget2 = new Render2Texture(Viewport.Width, Viewport.Height);
            DepthMapTarget = new Render2Texture(Viewport.Width, Viewport.Height);
        }

        public void ApplyPreferences(SynapseGaming.LightingSystem.Core.ILightingSystemPreferences preferences)
        {
        }        

        public bool BeginFrameRendering(SynapseGaming.LightingSystem.Core.ISceneState scenestate, int rendertargetindex)
        {
            if (!SunBurnRender.LastWasBegin)
            {
                SunBurnRender.BeginRender();
            }

            return true;           
        }
        public void EndFrameRendering()
        {
            if (SunBurnRender.LastWasBegin)
            {
                SunBurnRender.EndRender();
            }
            else
            {
                return;
            }

            Engine.GraphicsDevice.Clear(Color.Black);

            // Step 1: Blur the Screen horizontally
            SetBlurEffectParameters(1.0f / (float)SunBurnRender.RenderTarget.Width, 0);
            DrawFullscreenQuad(SunBurnRender.RenderTarget, RenderTarget1.RenderTarget,
                               GaussianBlurEffect);

            //Step 2: Blur Horizontally
            SetBlurEffectParameters(0, 1.0f / (float)SunBurnRender.RenderTarget.Height);
            DrawFullscreenQuad(RenderTarget1.RenderTarget, RenderTarget2.RenderTarget,
                               GaussianBlurEffect);

            //Step 3: Draw the Image blurred
            Engine.GraphicsDevice.SetRenderTarget(null);

            DepthMapTarget.BeginRender();
            CompareFunction initialFunction = Engine.GraphicsDevice.DepthStencilState.DepthBufferFunction;
            Engine.GraphicsDevice.Clear(Color.Black);
            DepthMapEffect.Parameters["MaxDepth"].SetValue(Camera.FarPlane);
            for (int i = 0; i < EngineSystem.Actors.Count; i++)
            {
                if (EngineSystem.Actors[i].Visible)
                {
                    EngineSystem.Actors[i].DrawWithCustomEffect(this.Camera, DepthMapEffect, "DepthMap",
                        "View", "Projection", "World");
                }
            }
            DepthMapTarget.EndRender();
            
            Engine.GraphicsDevice.Clear(Color.Black);

            Engine.GraphicsDevice.Textures[0] = SunBurnRender.RenderTarget;
            Engine.GraphicsDevice.Textures[1] = DepthMapTarget.RenderTarget;
            Engine.GraphicsDevice.Textures[2] = RenderTarget2.RenderTarget;
            DrawFullscreenQuad(SunBurnRender.RenderTarget, SunBurnRender.RenderTarget.Width,
                SunBurnRender.RenderTarget.Height, DepthOfFieldEffect);
        }

        /// <summary>
        /// Computes sample weightings and texture coordinate offsets
        /// for one pass of a separable gaussian blur filter.
        /// </summary>
        void SetBlurEffectParameters(float dx, float dy)
        {
            // Look up the sample weight and offset effect parameters.
            EffectParameter weightsParameter, offsetsParameter;

            weightsParameter = GaussianBlurEffect.Parameters["SampleWeights"];
            offsetsParameter = GaussianBlurEffect.Parameters["SampleOffsets"];

            // Look up how many samples our gaussian blur effect supports.
            int sampleCount = weightsParameter.Elements.Count;

            // Create temporary arrays for computing our filter settings.
            float[] sampleWeights = new float[sampleCount];
            Vector2[] sampleOffsets = new Vector2[sampleCount];

            // The first sample always has a zero offset.
            sampleWeights[0] = ComputeGaussian(0);
            sampleOffsets[0] = new Vector2(0);

            // Maintain a sum of all the weighting values.
            float totalWeights = sampleWeights[0];

            // Add pairs of additional sample taps, positioned
            // along a line in both directions from the center.
            for (int i = 0; i < sampleCount / 2; i++)
            {
                // Store weights for the positive and negative taps.
                float weight = ComputeGaussian(i + 1);

                sampleWeights[i * 2 + 1] = weight;
                sampleWeights[i * 2 + 2] = weight;

                totalWeights += weight * 2;

                // To get the maximum amount of blurring from a limited number of
                // pixel shader samples, we take advantage of the bilinear filtering
                // hardware inside the texture fetch unit. If we position our texture
                // coordinates exactly halfway between two texels, the filtering unit
                // will average them for us, giving two samples for the price of one.
                // This allows us to step in units of two texels per sample, rather
                // than just one at a time. The 1.5 offset kicks things off by
                // positioning us nicely in between two texels.
                float sampleOffset = i * 2 + 1.5f;

                Vector2 delta = new Vector2(dx, dy) * sampleOffset;

                // Store texture coordinate offsets for the positive and negative taps.
                sampleOffsets[i * 2 + 1] = delta;
                sampleOffsets[i * 2 + 2] = -delta;
            }

            // Normalize the list of sample weightings, so they will always sum to one.
            for (int i = 0; i < sampleWeights.Length; i++)
            {
                sampleWeights[i] /= totalWeights;
            }

            // Tell the effect about our new filter settings.
            weightsParameter.SetValue(sampleWeights);
            offsetsParameter.SetValue(sampleOffsets);
        }
        
        /// <summary>
        /// Evaluates a single point on the gaussian falloff curve.
        /// Used for setting up the blur filter weightings.
        /// </summary>
        float ComputeGaussian(float n)
        {
            float theta = BlurAmount;

            return (float)((1.0 / Math.Sqrt(2 * Math.PI * theta)) *
                           Math.Exp(-(n * n) / (2 * theta * theta)));
        }

        /// <summary>
        /// Helper for drawing a texture into a rendertarget, using
        /// a custom shader to apply postprocessing effects.
        /// </summary>
        void DrawFullscreenQuad(Texture2D texture, RenderTarget2D renderTarget,
                                Effect effect)
        {
            Engine.GraphicsDevice.SetRenderTarget(renderTarget);

            DrawFullscreenQuad(texture,
                               renderTarget.Width, renderTarget.Height,
                               effect);
        }


        /// <summary>
        /// Helper for drawing a texture into the current rendertarget,
        /// using a custom shader to apply postprocessing effects.
        /// </summary>
        void DrawFullscreenQuad(Texture2D texture, int width, int height,
                                Effect effect)
        {
            Engine.RenderManager.SpriteBatch.Begin(0, BlendState.Opaque, null, null, null, effect);
            Engine.RenderManager.SpriteBatch.Draw(texture, new Rectangle(0, 0, width, height), Color.White);
            Engine.RenderManager.SpriteBatch.End();
        }

        public void Unload()
        {
            //this.DepthMapEffect.Dispose();
            //this.DepthMapTarget.Disable();
            //this.DepthOfFieldEffect.Dispose();
        }

        public override void SerializeData()
        {
            base.SerializeData();

            Data.AddData("BlurUtility.BlurAmount", this.BlurAmount);
            Data.AddData("BlurUtility.Distance", this.Distance);
            Data.AddData("BlurUtility.Range", this.Range);
            Data.AddData("BlurUtility.Renderer", this.Renderer.Name);

            Animation.SerializeData(0, Data);
            DistanceAnimation.SerializeData(1, Data);
            RangeAnimation.SerializeData(2, Data);
        }
        public override void DeserializeData()
        {
            base.DeserializeData();

            this.BlurAmount = Data.GetData<float>("BlurUtility.BlurAmount");
            this.Distance = Data.GetData<float>("BlurUtility.Distance");
            this.Range = Data.GetData<float>("BlurUtility.Range");
            this.Renderer = (Renderer_SunBurn)EngineSystem.GetRenderer(Data.GetData<string>("BlurUtility.Renderer"));

            Animation.DeserializeData(0, Data);
            DistanceAnimation.DeserializeData(1, Data);
            RangeAnimation.DeserializeData(2, Data);

            Renderer.SceneInterface.PostProcessManager.AddPostProcessor(this);

            this.Representation = new EditorRepresentation("Engine\\Models\\model_post_processor", Renderer);
            this.Representation.Actor.ComponentParent = this;
            this.Representation.Actor.Parent = this;
        }
    }
}
